package terrain;


import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Set;

import deroulementjeu.Equipe;

import unite.TypeUnite;
import unite.Unite;


/**
 * Représente une case du Terrain
 * @author Alexis BRASY, Geoffrey LEQUESNE, Sandrine PIRIOU, Michel POPOV
 *
 */
public class CaseTerrain implements Serializable {

    private static final long serialVersionUID = 1L;
	
    private TypeTerrain typeTerrain;
    private Coordonnees coordonnees;
    private Terrain terrain;
    private Unite unite;
	
    /**
     * Constructeur d'une CaseTerrain avec 4 paramètres
     * @param type
     * @param coord
     * @param terrain
     * @param unit
     */
    public CaseTerrain(TypeTerrain type, Coordonnees coord, Terrain terrain, Unite unit) {
        this.typeTerrain = type;
        this.coordonnees = coord;
        this.terrain = terrain;
        this.unite = unit;
    }
	
    /**
     * Constructeur d'une case terrain, sans unité et avec un terrain de type vide.
     * @param coord Coordonnées de la case créée.
     * @param terrain Terrain dans lequel est situé cette case.
     */
    public CaseTerrain(Coordonnees coord, Terrain terrain) {
        this(TypeTerrain.PLAINE, coord, terrain, null);
    }
	
    /**
     * Getter du type de terrain
     * @return le type de terrain
     */
    public TypeTerrain getTypeTerrain() {
        return typeTerrain;
    }

    /**
     * Setter du type de terrain
     * @param typeTerrain
     */
    public void setTypeTerrain(TypeTerrain typeTerrain) {
        this.typeTerrain = typeTerrain;
    }

    /**
     * Getter des coordonnées de la case
     * @return coordonnées de la CaseTerrain
     */
    public Coordonnees getCoordonnees() {
        return coordonnees;
    }

    /**
     * Setter des coordonnées de la case
     * @param coordonnees
     */
    public void setCoordonnees(Coordonnees coordonnees) {
        this.coordonnees = coordonnees;
    }

    /**
     * Getter du Terrain qui contient la case
     * @return le terrain qui contient cette CaseTerrain
     */
    public Terrain getTerrain() {
        return terrain;
    }

    /**
     * Setter du Terrain qui contiendra la case
     * @param terrain
     * (peut-être pas utilisée plus tard)
     */
    public void setTerrain(Terrain terrain) {
        this.terrain = terrain;
    }

    /**
     * Getter de l'unité positionnée sur la case
     * @return l'unité positionnée sur la case
     */
    public Unite getUnite() {
        return unite;
    }

    /**
     * Met le champ unité à null. Enlève aussi l'unité de l'équipe
     */
    public void retirerUnite() {
        unite.getEquipe().retirerUnite(this.unite);
        this.unite = null;
    }
	
    /**
     * Met le champ unité à null.
     */
    public void retirerUniteSansRetirerEquipe() {
        this.unite = null;
    }
	
    /**
     * Ajoute une unité sur une case ne contenant pas d'unité.
     * @param unite Nouvelle unité devant occuper la case.
     */
    public void ajouterUnite(Unite unite) {
        this.unite = unite;
        unite.setCaseTerrain(this);
    }
	
    /**
     * Renvoie le coût d'accès à la CaseTerrain, à vol d'oiseau (distance entre deux cases)
     * /!\ fonction assez gourmande
     * @param c CaseTerrain dont on cherche le coût d'accès.
     * @return Le coût d'accès.
     */
    public int coutDAccesVolDOiseau(CaseTerrain c) {
        // Calcul de la distance entre deux cases :
        // comme les formules appliquées sur internet ne fonctionnent pas, utilisation du calcul
        // de coût d'accès à une case en utilisant un terrain constitué entièrement de plaine
        // (coût d'accès = 0 pour toutes les unités)
		
        // création du terrain factice
        Terrain t = new Terrain(this.getTerrain().getNbLignes(),
                this.getTerrain().getNbColonnes(), this.getTerrain().getRegles());
		
        HashMap<Coordonnees, Integer> couts = new HashMap<Coordonnees, Integer>();
        HashMap<Coordonnees, CaseTerrain> chemin = new HashMap<Coordonnees, CaseTerrain>();

        calculCoutDAccesCase(couts, chemin, TypeUnite.EAU,
                t.getCase(c.getCoordonnees()), null, 0);
		
        return couts.get(t.getCase(this.coordonnees).getCoordonnees()).intValue();
    }
	
    /**
     * Calcule récursivement le coût d'accès minimum à chaque case pour une unité donnée
     * @param couts contient les coûts d'accès minimum pour chaque case
     * @param chemin contient la case précédente dans l'itinéraire au coût minimum
     * @param typeU type de l'unité concernée
     * @param courante case dont on calcule le coût
     * @param prec case dont on vient
     * @param deplacementIntermediaire coût de la case précédente
     */
    public static void calculCoutDAccesCase(HashMap<Coordonnees, Integer> couts, HashMap<Coordonnees, CaseTerrain> chemin, TypeUnite typeU, CaseTerrain courante, CaseTerrain prec, int deplacementIntermediaire) {
        int cout;
		
        // Calcul du coût de la case 
        // coût = coût intermédiaire + coût de la case courante dans le cas où il y a effectivement un itinéraire parcouru
        if (prec != null) {
            cout = deplacementIntermediaire
                    + courante.getTerrain().getRegles().getCoutDepl(
                            courante.getTypeTerrain(), typeU);
        } else {
            // coût = 0 si on traite la case sur laquelle se trouve l'unité (prec == null)
            // (fait pour qu'on puisse bien insérer le bon coût lors de l'initialisation en
            // appelant ensuite les cases adjacentes
            cout = 0;
        }
		
        // Traitement à faire si on a déjà un coût dans la map et qu'il est supérieur au coût
        // qu'on vient de calculer ou qu'il n'y a aucun coût enregistré
        if (couts.containsKey(courante.getCoordonnees())
                && couts.get(courante.getCoordonnees()).intValue() > cout
                        || !couts.containsKey(courante.getCoordonnees())) {
            couts.put(courante.getCoordonnees(), new Integer(cout));
            chemin.put(courante.getCoordonnees(), prec);
            // Appel de la fonction sur toutes les cases adjacentes de la case
            Collection<CaseTerrain> adj = courante.getCaseAdjacente();

            for (CaseTerrain caseT : adj) {
                calculCoutDAccesCase(couts, chemin, typeU, caseT, courante, cout);
            }
        }

    }

    /**
     * Renvoie le coût d'accès (déplacement) de la case en fonction de
     * l'unité unit.
     * @param unit l'unité concernée par le calcul du coût d'accès
     * @param itineraire l'itinéraire optimal depuis la case où se trouve l'unité jusqu'à la case d'arrivée
     * @param champAction l'ensemble des cases accessibles par l'unité en un tour
     * @return le coût d'accès
     */
    public int coutDAcces(Unite unit, Itineraire itineraire, ArrayList<CaseTerrain> champAction) {
        // Calcul du plus petit coût d'accès
        HashMap<Coordonnees, Integer> couts = new HashMap<Coordonnees, Integer>();
        HashMap<Coordonnees, CaseTerrain> chemin = new HashMap<Coordonnees, CaseTerrain>();

        calculCoutDAccesCase(couts, chemin, unit.getType(),
                unit.getCaseTerrain(), null, 0);
		
        // Calcul du champ d'action
        // = ensemble des cases dont le cout d'accès est inférieur à la capacité de déplacement
        // de l'unité
        if (champAction != null) {
            Set<Coordonnees> coord = couts.keySet();
            Iterator<Coordonnees> it = coord.iterator();

            while (it.hasNext()) {
                Coordonnees c = (Coordonnees) it.next();

                if (couts.get(c).intValue() <= unit.getDeplacement()) {
                    champAction.add(
                            unit.getCaseTerrain().getTerrain().getCase(c));
                }
            }
        }

        // Calcul de l'itinéraire si besoin est (itineraire != null)
        // - vider l'itinéraire déjà initialisé
        // - créer l'itinéraire à l'envers en partant de la case d'arrivée
        // et en remontant jusqu'à la case de départ grâce à chemin.
        // - inverser l'itinéraire
        if (itineraire != null) {
            Coordonnees intermediaire = this.coordonnees;
			
            // Vider l'itinéraire déjà initialisé
            itineraire.reinitialiserCompletement();
			
            // Récupération de toutes les cases à parcourir (sauf de la case initiale)
            while (chemin.containsKey(intermediaire)
                    && chemin.get(intermediaire) != null) {
                itineraire.ajouterCaseSansDepl(intermediaire);
                intermediaire = chemin.get(intermediaire).getCoordonnees();
            }
            // Ajout de la case initiale
            itineraire.ajouterCaseSansDepl(unit.getCaseTerrain().coordonnees);
			
            // Inversion de l'itinéraire
            itineraire.inverser();
        }
		
        if (couts.containsKey(this.coordonnees)) {
            return couts.get(this.coordonnees).intValue();
        } else {
            return -1;
        }
    }
	
    /**
     * Renvoie le booléen si la case est accessible (true) ou pas (false)
     * en fonction de l'unité unit
     * @param unit
     * @return boolean true ou false.
     */
    public boolean caseAccessible(Unite unit) {
        if (coutDAcces(unit, null, null) <= unit.getDeplacement()) {
            return true;
        } else {
            return false;
        }
    }
	
    /**
     * Renvoie la collection contenant toutes les cases adjacentes de cette case
     * Si on est sur une ligne impaire :
     * 		(x-1,y), (x-1, y+1), (x, y-1), (x, y+1), (x+1, y), (x+1, y+1)
     * Si on est sur une ligne paire :
     * 		(x-1, y-1), (x-1, y), (x, y-1), (x, y+1), (x+1, y-1), (x+1, y)
     * @return la liste des cases
     */
    public Collection<CaseTerrain> getCaseAdjacente() {
        Collection <CaseTerrain> ret = new ArrayList<CaseTerrain>();
		
        // Cases communes aux lignes paires et impaires :
        // (x-1, y), (x, y-1), (x, y+1), (x+1, y)
        if (coordonnees.getX() + 1 < terrain.getNbLignes()) {
            ret.add(
                    terrain.getCase(
                            new Coordonnees(coordonnees.getX() + 1,
                            coordonnees.getY())));
        }
        if (coordonnees.getX() - 1 >= 0) {
            ret.add(
                    terrain.getCase(
                            new Coordonnees(coordonnees.getX() - 1,
                            coordonnees.getY())));
        }
        if (coordonnees.getY() + 1 < terrain.getNbColonnes()) {
            ret.add(
                    terrain.getCase(
                            new Coordonnees(coordonnees.getX(),
                            coordonnees.getY() + 1)));
        }
        if (coordonnees.getY() - 1 >= 0) {
            ret.add(
                    terrain.getCase(
                            new Coordonnees(coordonnees.getX(),
                            coordonnees.getY() - 1)));
        }
		
        // Cases pour les lignes impaires
        // (x-1, y+1) et (x+1, y+1)
        if (coordonnees.getX() % 2 == 1) {
            if (coordonnees.getX() - 1 >= 0
                    && coordonnees.getY() + 1 < terrain.getNbColonnes()) {
                ret.add(
                        terrain.getCase(
                                new Coordonnees(coordonnees.getX() - 1,
                                coordonnees.getY() + 1)));
            }
            if (coordonnees.getX() + 1 < terrain.getNbLignes()
                    && coordonnees.getY() + 1 < terrain.getNbColonnes()) {
                ret.add(
                        terrain.getCase(
                                new Coordonnees(coordonnees.getX() + 1,
                                coordonnees.getY() + 1)));
            }
        } // Cases pour les lignes paires
        // (x-1, y-1) et (x-1, y-1)
        else {
            if (coordonnees.getX() - 1 >= 0 && coordonnees.getY() - 1 >= 0) {
                ret.add(
                        terrain.getCase(
                                new Coordonnees(coordonnees.getX() - 1,
                                coordonnees.getY() - 1)));
            }
            if (coordonnees.getX() + 1 < terrain.getNbLignes()
                    && coordonnees.getY() - 1 >= 0) {
                ret.add(
                        terrain.getCase(
                                new Coordonnees(coordonnees.getX() + 1,
                                coordonnees.getY() - 1)));
            }
        }
        return ret;
    }
	
    /**
     * Renvoie le booléen correspondant au fait que la case est vide (true)
     * ou qu'il y a une unité dessus (false)
     * @return le booléen
     */
    public boolean caseVide() {
        if (unite == null) {
            return true;
        } else {
            return false;
        }
    }
	
    /**
     * Vérifie que la case terrain passée en paramètre est adjacente à la case courante
     * @param caseT la case terrain dont on vérifie la proximité
     * @return true si les deux cases sont adjacentes
     */
    public boolean caseAdjacente(CaseTerrain caseT) {
        Collection<CaseTerrain> adj = caseT.getCaseAdjacente();

        if (adj.contains(this)) {
            return true;
        } else {
            return false;
        }
    }
	
    /**
     * Méthode qui permet de savoir si la case est visible ou non, après application du brouillard de guerre
     * @param equipe équipe qui est en train de jouer
     * @return true si la case est visible, malgré le brouillard de guerre
     */
    public boolean estVisiblePar(Equipe equipe) {
        ArrayList<Unite> unites = equipe.getUnites();
        Iterator<Unite> it = unites.iterator();
		
        while (it.hasNext()) {
            Unite u = it.next(); 

            /*
            System.out.println(
                    "u : " + u.getCaseTerrain().getCoordonnees().getX() + " ; "
                    + u.getCaseTerrain().getCoordonnees().getY());
			
            System.out.println(
                    "c : " + this.getCoordonnees().getX() + " ; "
                    + this.getCoordonnees().getY());
            */
			
            if (u.getCaseTerrain().coutDAccesVolDOiseau(this) <= u.getVision()) {
                
                return true;
            }
				
            if (u.getCaseTerrain() == this) {
                
                return true;
            }
				
        }
        
        return false;
    }
}
